package hu.ikti.obr.sync.manager.service.impl;

import hu.ikti.oamadmin.types.entities.Bundle;
import hu.ikti.oamadmin.types.entities.BundleData;
import hu.ikti.oamadmin.types.entities.ByteCode;
import hu.ikti.oamadmin.types.entities.Capability;
import hu.ikti.oamadmin.types.entities.Repository;
import hu.ikti.oamadmin.types.entities.Requirement;
import hu.ikti.oamadmin.types.entities.Version;
import hu.ikti.obr.sync.manager.service.SynchronisationManager;

import java.net.URL;
import java.util.Calendar;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.persistence.Query;

import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.service.obr.RepositoryAdmin;
import org.osgi.service.obr.Resource;

public class SynchronisationManagerImpl implements SynchronisationManager {
	
	// TODO: close database connection!!!

	private BundleContext context;
	private RepositoryAdmin repositoryAdmin;

	private EntityManager em;
	private EntityManagerFactory emf;

	private Set<BundleData> newBundles = new HashSet<BundleData>();
	private Set<BundleData> storedBundles = new HashSet<BundleData>();

	private Set<Capability> allCapabilities = new HashSet<Capability>();

	private org.osgi.framework.Bundle[] runningBundles;

	private Repository repository;

	public SynchronisationManagerImpl(RepositoryAdmin repoAdmin,
			BundleContext context) {
		emf = Persistence.createEntityManagerFactory("OAMAdmin.jpa");
		em = emf.createEntityManager();

		repositoryAdmin = repoAdmin;
		this.context = context;

		runningBundles = context.getBundles();
	}

	public void synchronise(Long repoId) throws Exception {
		repository = loadRepository(repoId);

		// Repository doesn't exist
		if (repository == null) {
			throw new Exception("No such repository");
		}

		loadStoredBundles(repository);

		repositoryAdmin.addRepository(new URL(repository.getUrl()));

		Resource[] resources = repositoryAdmin.discoverResources(null);

		// Iterate over the new repository bundles
		for (Resource resource : resources) {
			System.out.println(".....processing..... "
					+ resource.getSymbolicName() + " - "
					+ resource.getVersion());

			// Map Resource to BundleData
			BundleData bundleData = new BundleData();
			bundleData.setSymbolicName(resource.getSymbolicName());
			bundleData.setPresentationName(resource.getPresentationName());
			bundleData.setVersion(new Version(resource.getVersion()));
			bundleData.setUrl(resource.getURL().getPath());
			bundleData.setUploadTime(Calendar.getInstance().getTime());
			bundleData.setRepository(repository);

			// In case of invalid bundle url, skip processing
			try {
				bundleData.setByteCode(new ByteCode(resource.getURL()));
			} catch (Exception e) {
				System.out.println("Invalid bundle url - skip processing: "
						+ resource.getSymbolicName() + " - "
						+ resource.getVersion());
				continue;
			}

			// Process bundle's manifest header
			org.osgi.framework.Bundle bundle = null;

			try {
				for (org.osgi.framework.Bundle b : runningBundles) {
					if (resource.getSymbolicName().equals(b.getSymbolicName())
							&& resource.getVersion().equals(b.getVersion())) {
						bundle = b;
						System.out.println("Bundle already installed: "
								+ b.getSymbolicName() + " - " + b.getVersion());
						bundleData.setManifest(processManifest(bundle
								.getHeaders()));
						break;
					}
				}

				if (bundle == null) {
					String link = resource.getURL().getProtocol() + "://"
							+ resource.getURL().getHost()
							+ resource.getURL().getPath();
					bundle = context.installBundle(link);
					bundleData
							.setManifest(processManifest(bundle.getHeaders()));
					bundle.uninstall();
				}
			} catch (BundleException be) {
				System.out.println("Error processing bundle's manifest");
			}

			// Process bundle's capabilities
			// TODO: capability processing limited to packages only
			org.osgi.service.obr.Capability[] caps = resource.getCapabilities();
			for (org.osgi.service.obr.Capability c : caps) {
				if (c.getName().equals("package")) {
					Capability capability = new Capability();
					capability.getBundleDatas().add(bundleData);

					Map<?, ?> properties = c.getProperties();
					Set<?> keys = properties.keySet();
					for (Object object : keys) {
						String key = (String) object;
						if (key.equals("package")) {
							capability.setPackageName((String) properties
									.get(key));
						} else if (key.equals("version")) {
							capability.setVersion(new Version(
									(org.osgi.framework.Version) properties
											.get(key)));
						}
						// TODO: equals("uses")
					}

					// Capability already stored at database
					Query query = em
							.createQuery("select capability from Capability capability where capability.packageName = :packageName and capability.version = :version");
					query.setParameter("packageName",
							capability.getPackageName());
					query.setParameter("version", capability.getVersion());

					try {
						Capability cap = (Capability) query.getSingleResult();
						bundleData.getCapabilities().add(cap);
					} catch (Exception e) {
						// Capability already found while synchronizing
						if (!allCapabilities.contains(capability)) {
							bundleData.getCapabilities().add(capability);
							allCapabilities.add(capability);
						} else {
							Object[] objects = allCapabilities.toArray();
							for (Object object : objects) {
								if (((Capability) object).equals(capability)) {
									bundleData.getCapabilities().add(
											(Capability) object);
								}
							}
						}
					}
				}
			}

			// Process bundle's requirements
			org.osgi.service.obr.Requirement[] reqs = resource
					.getRequirements();
			for (org.osgi.service.obr.Requirement r : reqs) {
				Requirement requirement = new Requirement();
				requirement.getBundleDatas().add(bundleData);
				requirement.setFilter(r.getFilter());
				requirement.setName(r.getName());
				requirement.setExtend(r.isExtend());
				requirement.setMultiple(r.isMultiple());
				requirement.setOptional(r.isOptional());
				requirement.setComment(r.getComment());

				bundleData.getRequirements().add(requirement);
			}

			if (!storedBundles.contains(bundleData)) {
				newBundles.add(bundleData);
			} else {
				storedBundles.remove(bundleData);
			}
		}

		// Update the database
		updateDatabase();
	}

	private void loadStoredBundles(Repository repo) {
		Query query = em
				.createQuery("select bundleData from BundleData bundleData where bundleData.deleted = false and bundleData.repository = :repo");
		List<?> results = query.setParameter("repo", repo).getResultList();

		System.out.println();
		System.out.println("--- Stored bundles ---");

		for (Object object : results) {
			storedBundles.add((BundleData) object);
			System.out.println("\t" + ((BundleData) object).getSymbolicName()
					+ " - " + ((BundleData) object).getVersion());
		}

		if (results.size() == 0) {
			System.out.println("none\n");
		}
	}

	private Repository loadRepository(Long id) {
		try {
			Repository repository = (Repository) em.find(Repository.class, id);
			return repository;
		} catch (Exception e) {
			return null;
		}
	}

	private void updateDatabase() {
		em.getTransaction().begin();

		System.out.println("--- Added bundles ---");
		for (BundleData bundleData : newBundles) {
			Bundle bundle = new Bundle();
			bundle.setBundleData(bundleData);
			em.persist(bundle);
			System.out.println("\t" + bundleData.getSymbolicName() + " - "
					+ bundleData.getVersion());
		}

		System.out.println("--- Bundles marked deleted ---");
		for (BundleData bundleData : storedBundles) {
			bundleData.setDeleted(true);
			em.merge(bundleData);
			System.out.println("\t" + bundleData.getSymbolicName() + " - "
					+ bundleData.getVersion());
		}

		repository.setLastSync(Calendar.getInstance().getTime());

		em.getTransaction().commit();

		System.out.println("Changes saved successfully...");
	}

	private String processManifest(Dictionary<?, ?> manifest) {
		Enumeration<?> keys = manifest.keys();
		String manifestText = "";
		while (keys.hasMoreElements()) {
			String key = (String) keys.nextElement();
			manifestText += key + "\n\t" + manifest.get(key) + "\n";
		}
		return manifestText;
	}

	public void bindRepositoryAdmin(RepositoryAdmin repoAdmin) {
		repositoryAdmin = repoAdmin;
	}

	public void unbindRepositoryAdmin() {
		repositoryAdmin = null;
	}

}
